rentease-backend-new/controllers/rentalController.js

1464 lines
43 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const { Rental, Room, Apartment, Bill, Transaction, Renter, MeterReading } = require('../models');
const { Op } = require('sequelize');
const response = require('../utils/response');
const rentalBillingService = require('../services/rentalBillingService');
// 格式化时间(考虑时区,转换为北京时间)
const formatDate = (date) => {
if (!date) return null;
const dateObj = date instanceof Date ? date : new Date(date);
if (isNaN(dateObj.getTime())) return null;
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);
if (isNaN(dateObj.getTime())) return null;
const beijingDate = new Date(dateObj.getTime() + 8 * 60 * 60 * 1000);
return beijingDate.toISOString().replace('T', ' ').slice(0, 19);
};
// 格式化租房数据
const formatRentalData = (rental) => {
const formattedRental = {
...rental.toJSON(),
startDate: formatDate(rental.startDate),
endDate: formatDate(rental.endDate),
createTime: formatDateTime(rental.createTime),
updateTime: formatDateTime(rental.updateTime)
};
return formattedRental;
};
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(Math.random() * 10000).toString().padStart(4, '0');
return `B${year}${month}${day}${random}`;
};
const buildBillingMeta = (settlementType, sourceEventId = null) => ({
settlementType,
sourceEventId
});
// 检查并更新租房状态
const checkAndUpdateRentalStatus = async (tenantId) => {
try {
// 获取当前日期
const currentDate = new Date();
// 计算5天后的日期
const fiveDaysLater = new Date();
fiveDaysLater.setDate(currentDate.getDate() + 5);
// 查找该租户下所有活跃的租房记录
const rentals = await Rental.findAll({
where: { tenantId, status: 'active', isDeleted: 0 },
include: [
{
model: Room,
where: { isDeleted: 0 }
}
]
});
// 检查每个租房记录的状态
for (const rental of rentals) {
const endDate = new Date(rental.endDate);
// 检查是否已到期
if (endDate < currentDate) {
// 更新房间租约状态为已到期
const room = await Room.findByPk(rental.roomId);
if (room && room.status === 'rented') {
await room.update({ rentalStatus: 'expired' });
}
} else if (endDate <= fiveDaysLater) {
// 更新房间租约状态为即将到期
const room = await Room.findByPk(rental.roomId);
if (room && room.status === 'rented') {
await room.update({ rentalStatus: 'soon_expire' });
}
} else {
// 更新房间租约状态为正常
const room = await Room.findByPk(rental.roomId);
if (room && room.status === 'rented') {
await room.update({ rentalStatus: 'normal' });
}
}
}
console.log('租房状态检查和更新完成');
} catch (error) {
console.error('检查和更新租房状态时出错:', error);
}
};
// 获取所有租房(支持搜索和分页)
const getAllRentals = async (req, res) => {
try {
// 先检查并更新租房状态
await checkAndUpdateRentalStatus(req.user.tenantId);
const {
apartmentId,
roomId,
renterName,
status,
startDateFrom,
startDateTo,
endDateFrom,
endDateTo,
page = 1,
pageSize = 10
} = req.query;
// 构建查询条件
const where = { tenantId: req.user.tenantId, isDeleted: 0 };
if (status) {
where.status = status;
}
if (roomId) {
where.roomId = roomId;
}
if (startDateFrom && startDateTo) {
where.startDate = { [Op.between]: [new Date(startDateFrom), new Date(startDateTo)] };
}
if (endDateFrom && endDateTo) {
where.endDate = { [Op.between]: [new Date(endDateFrom), new Date(endDateTo)] };
}
// 构建包含关系
const include = [
{
model: Room,
where: {
isDeleted: 0,
...(apartmentId ? { apartmentId } : {})
},
include: [
{
model: Apartment,
where: { isDeleted: 0 }
}
]
},
{
model: Renter,
where: renterName ? { name: { [Op.like]: `%${renterName}%` } } : undefined,
required: !!renterName
}
];
// 计算偏移量
const offset = (page - 1) * pageSize;
// 查询租房数据
const { count, rows } = await Rental.findAndCountAll({
where,
include,
limit: parseInt(pageSize),
offset: parseInt(offset),
order: [['createTime', 'DESC']] // 按创建时间倒序排序
});
// 格式化数据
const formattedRentals = rows.map(formatRentalData);
// 返回结果
response.success(res, '获取成功', {
list: formattedRentals,
total: count,
page: parseInt(page),
pageSize: parseInt(pageSize)
});
} catch (error) {
response.serverError(res, '获取租房列表失败', error);
}
};
// 获取单个租房
const getRentalById = async (req, res) => {
try {
const { id } = req.params;
const rental = await Rental.findOne({
where: { id, tenantId: req.user.tenantId, isDeleted: 0 },
include: [
{
model: Room,
where: { isDeleted: 0 },
include: [
{
model: Apartment
}
]
},
{
model: Renter
}
]
});
if (!rental) {
return response.notFound(res, '租房记录不存在');
}
const formattedRental = formatRentalData(rental);
response.success(res, '获取成功', formattedRental);
} catch (error) {
response.serverError(res, '获取租房详情失败', error);
}
};
// 创建租房
const createRental = async (req, res) => {
try {
console.log('接收到的请求数据:', req.body);
// 直接使用req.body中的数据
const body = req.body;
// 检查请求体是否存在
if (!body) {
return response.badRequest(res, '请求体不能为空');
}
// 检查所有必要参数
if (!body.roomId) {
return response.badRequest(res, '缺少房间ID');
}
if (!body.renterId) {
return response.badRequest(res, '缺少租客ID');
}
if (!body.startDate) {
return response.badRequest(res, '缺少开始日期');
}
if (!body.endDate) {
return response.badRequest(res, '缺少结束日期');
}
if (!body.rent) {
return response.badRequest(res, '缺少租金');
}
// 转换roomId和renterId为整数类型
const parsedRoomId = parseInt(body.roomId);
const parsedRenterId = parseInt(body.renterId);
if (isNaN(parsedRoomId)) {
return response.badRequest(res, '无效的房间ID');
}
if (isNaN(parsedRenterId)) {
return response.badRequest(res, '无效的租客ID');
}
// 检查房间是否存在
const room = await Room.findByPk(parsedRoomId);
if (!room) {
return response.badRequest(res, '房间不存在');
}
// 检查房间是否有未结束的租赁合同
const activeRental = await Rental.findOne({
where: {
roomId: parsedRoomId,
status: { [Op.in]: ['active', 'reserved'] },
endDate: { [Op.gte]: new Date() }
}
});
if (activeRental) {
return response.badRequest(res, '该房间已有进行中的租赁合同');
}
// 处理押金为空时设置为0
const deposit = body.deposit || 0;
// 获取租客信息用于账单备注
const renter = await Renter.findByPk(parsedRenterId);
const renterName = renter ? renter.name : '';
var transaction;
// 创建租房记录
console.log('创建租房记录:', {
roomId: parsedRoomId,
renterId: parsedRenterId,
startDate: body.startDate,
endDate: body.endDate,
paymentType: body.paymentType || 'monthly',
rent: body.rent,
deposit: deposit,
operator: body.operator,
waterMeterStart: body.waterMeterStart,
electricityMeterStart: body.electricityMeterStart,
status: body.status || 'active',
remark: body.remark
});
transaction = await require('../config/db').transaction();
const rental = await Rental.create({
roomId: parsedRoomId,
renterId: parsedRenterId,
startDate: body.startDate,
endDate: body.endDate,
paymentType: body.paymentType || 'monthly',
rent: body.rent,
deposit: deposit,
operator: body.operator || null,
waterMeterStart: body.waterMeterStart || null,
electricityMeterStart: body.electricityMeterStart || null,
status: body.status || 'active',
remark: body.remark,
tenantId: req.user.tenantId,
createBy: req.user.id,
updateBy: req.user.id
}, { transaction });
console.log('租房记录:', rental);
// 更新房间状态为已租
await Room.update({ status: 'rented', rentalStatus: 'normal' }, { where: { id: parsedRoomId }, transaction });
// 自动生成租金账单
await Bill.create({
billNo: 'B' + Date.now(),
rentalId: rental.id,
roomId: parsedRoomId,
renterId: parsedRenterId,
type: 'income',
category: 'rent',
receivableAmount: body.rent,
receivedAmount: 0,
status: 'unpaid',
billDate: body.startDate,
billMonth: body.startDate.substring(0, 7),
settlementType: 'normal',
remark: `租约租金 - ${renterName}`,
tenantId: req.user.tenantId,
createBy: req.user.id,
updateBy: req.user.id
}, { transaction });
// 自动生成押金账单(如果有押金)
if (deposit > 0) {
await Bill.create({
billNo: 'B' + Date.now() + '1',
rentalId: rental.id,
roomId: parsedRoomId,
renterId: parsedRenterId,
type: 'income',
category: 'deposit',
receivableAmount: deposit,
receivedAmount: 0,
status: 'unpaid',
billDate: body.startDate,
billMonth: body.startDate.substring(0, 7),
settlementType: 'normal',
remark: `租约押金 - ${renterName}`,
tenantId: req.user.tenantId,
createBy: req.user.id,
updateBy: req.user.id
}, { transaction });
}
await transaction.commit();
response.created(res, '创建成功', rental);
} catch (error) {
if (typeof transaction !== 'undefined' && transaction) {
await transaction.rollback();
}
console.error('创建租房记录时出错:', error);
response.serverError(res, '创建租房失败', error);
}
};
// 更新租房
const updateRental = async (req, res) => {
try {
const { id } = req.params;
const { roomId, renterId, startDate, endDate, paymentType, rent, deposit, operator, waterMeterStart, electricityMeterStart, waterMeterEnd, electricityMeterEnd, status, remark } = req.body;
const rental = await Rental.findOne({
where: { id, tenantId: req.user.tenantId, isDeleted: 0 },
include: [{ model: Room }]
});
if (!rental) {
return response.notFound(res, '租房记录不存在');
}
// 状态校验:已终止/已到期的租约不能编辑
if (rental.status === 'terminated' || rental.status === 'expired') {
return response.badRequest(res, '已终止或已到期的租约不能编辑');
}
const oldRental = rental.toJSON();
var transaction = await require('../config/db').transaction();
// 如果更换了房间,检查新房间是否有效
if (roomId && roomId !== rental.roomId) {
const newRoom = await Room.findOne({
where: { id: roomId, tenantId: req.user.tenantId, isDeleted: 0 }
});
if (!newRoom) {
await transaction.rollback();
return response.badRequest(res, '新房间不存在');
}
if (newRoom.status === 'rented') {
await transaction.rollback();
return response.badRequest(res, '新房间已在租');
}
// 更新旧房间状态为空房
await Room.update(
{ status: 'empty', rentalStatus: 'normal' },
{ where: { id: rental.roomId }, transaction }
);
// 更新新房间状态为已租
await Room.update(
{ status: 'rented', rentalStatus: 'normal' },
{ where: { id: roomId }, transaction }
);
}
// 处理押金为空时设置为0
const updateDeposit = deposit || 0;
await rental.update({
roomId,
renterId,
startDate,
endDate,
paymentType: paymentType || 'monthly',
rent,
deposit: updateDeposit,
operator: operator || null,
waterMeterStart: waterMeterStart || null,
electricityMeterStart: electricityMeterStart || null,
waterMeterEnd: waterMeterEnd || null,
electricityMeterEnd: electricityMeterEnd || null,
status,
remark,
updateBy: req.user.id
}, { transaction });
const hasPaidBills = await Bill.count({
where: {
rentalId: id,
tenantId: req.user.tenantId,
status: { [Op.in]: ['paid', 'partial'] },
isDeleted: 0
},
transaction
});
const financialChanged = String(oldRental.rent) !== String(rental.rent) || String(oldRental.deposit || 0) !== String(rental.deposit || 0) || String(oldRental.startDate) !== String(rental.startDate) || oldRental.roomId !== rental.roomId || oldRental.renterId !== rental.renterId;
if (hasPaidBills > 0 && financialChanged) {
await transaction.rollback();
return response.badRequest(res, '该租约已有收款记录,不能直接修改租金、押金、日期、房间或租客');
}
const renter = await Renter.findByPk(rental.renterId, { transaction });
await rentalBillingService.syncEditableInitialBills({
rental,
oldRental,
renterName: renter ? renter.name : '',
operatorId: req.user.id,
transaction
});
await transaction.commit();
response.success(res, '更新成功', rental);
} catch (error) {
if (typeof transaction !== 'undefined' && transaction) {
await transaction.rollback();
}
response.serverError(res, '更新租约失败', error);
}
};
// 删除租约(软删除)
const deleteRental = async (req, res) => {
const transaction = await require('../config/db').transaction();
try {
const { id } = req.params;
const rental = await Rental.findOne({
where: { id, tenantId: req.user.tenantId, isDeleted: 0 },
transaction
});
if (!rental) {
await transaction.rollback();
return response.notFound(res, '租约不存在');
}
await rentalBillingService.assertRentalCanBeDeleted({
rentalId: id,
tenantId: req.user.tenantId,
transaction
});
await rentalBillingService.cancelUnpaidBillsForRental({
rentalId: id,
tenantId: req.user.tenantId,
operatorId: req.user.id,
transaction
});
await rental.update({
isDeleted: 1,
updateBy: req.user.id
}, { transaction });
const otherActiveRental = await Rental.findOne({
where: {
id: { [Op.ne]: id },
roomId: rental.roomId,
tenantId: req.user.tenantId,
status: 'active',
isDeleted: 0
},
transaction
});
if (!otherActiveRental) {
await Room.update(
{ status: 'empty', rentalStatus: 'normal' },
{ where: { id: rental.roomId, tenantId: req.user.tenantId }, transaction }
);
}
await transaction.commit();
response.success(res, '租约删除成功');
} catch (error) {
await transaction.rollback();
if (error.statusCode === 400) {
return response.badRequest(res, error.message);
}
response.serverError(res, '删除租房失败', error);
}
};
// 退租处理(优化版:结算水电费 + 退还押金)
const terminateRental = async (req, res) => {
const transaction = await require('../config/db').transaction();
try {
const { id } = req.params;
const {
waterMeterEnd,
electricityMeterEnd,
refundDeposit,
remark
} = req.body;
const rental = await Rental.findOne({
where: { id, tenantId: req.user.tenantId, isDeleted: 0 },
include: [
{
model: Room,
where: { isDeleted: 0 },
include: [{ model: Apartment, where: { isDeleted: 0 } }]
},
{ model: Renter }
],
transaction
});
if (!rental) {
await transaction.rollback();
return response.notFound(res, '租约不存在');
}
if (rental.status !== 'active') {
await transaction.rollback();
return response.badRequest(res, '只有生效中的租约可以退租');
}
// 检查是否有未支付的租金/水电费账单
const unpaidBills = await Bill.findAll({
where: {
rentalId: id,
status: { [Op.in]: ['unpaid', 'partial'] },
category: { [Op.in]: ['rent', 'water', 'electricity'] },
isDeleted: 0
},
transaction
});
if (unpaidBills && unpaidBills.length > 0) {
await transaction.rollback();
return response.badRequest(res, `该租约有 ${unpaidBills.length} 笔未结清的账单,请先结清后再退租`);
}
const room = rental.Room;
const apartment = room.Apartment;
const renterName = rental.Renter ? rental.Renter.name : '';
const createBy = req.user.id;
const tenantId = req.user.tenantId;
const billsGenerated = [];
// 1. 更新租约信息
await rental.update({
status: 'terminated',
closeReason: 'terminated',
waterMeterEnd: waterMeterEnd || null,
electricityMeterEnd: electricityMeterEnd || null,
remark: remark ? `${rental.remark || ''}\n退租备注:${remark}` : rental.remark,
updateBy: createBy
}, { transaction });
// 2. 生成水电费账单(如有读数差异)
if (waterMeterEnd && rental.waterMeterStart) {
const waterUsage = parseFloat(waterMeterEnd) - parseFloat(rental.waterMeterStart);
if (waterUsage > 0) {
const waterAmount = waterUsage * parseFloat(apartment.waterPrice || 0);
const bill = await Bill.create({
billNo: generateBillNo(),
rentalId: id,
roomId: room.id,
renterId: rental.renterId,
type: 'income',
category: 'water',
receivableAmount: waterAmount,
receivedAmount: 0,
status: 'unpaid',
billDate: new Date(),
settlementType: 'checkout',
sourceEventId: id,
remark: `退租水费 - ${renterName}(用量:${waterUsage.toFixed(2)}吨)`,
tenantId,
createBy,
updateBy: createBy
}, { transaction });
billsGenerated.push({ category: 'water', amount: waterAmount });
}
}
if (electricityMeterEnd && rental.electricityMeterStart) {
const electricityUsage = parseFloat(electricityMeterEnd) - parseFloat(rental.electricityMeterStart);
if (electricityUsage > 0) {
const electricityAmount = electricityUsage * parseFloat(apartment.electricityPrice || 0);
const bill = await Bill.create({
billNo: generateBillNo(),
rentalId: id,
roomId: room.id,
renterId: rental.renterId,
type: 'income',
category: 'electricity',
receivableAmount: electricityAmount,
receivedAmount: 0,
status: 'unpaid',
billDate: new Date(),
settlementType: 'checkout',
sourceEventId: id,
remark: `退租电费 - ${renterName}(用量:${electricityUsage.toFixed(2)}度)`,
tenantId,
createBy,
updateBy: createBy
}, { transaction });
billsGenerated.push({ category: 'electricity', amount: electricityAmount });
}
}
// 3. 生成押金退还账单(支出类型)
const depositToRefund = refundDeposit !== undefined ? refundDeposit : (rental.deposit || 0);
if (depositToRefund > 0) {
const bill = await Bill.create({
billNo: generateBillNo(),
rentalId: id,
roomId: room.id,
renterId: rental.renterId,
type: 'expense',
category: 'deposit',
receivableAmount: depositToRefund,
receivedAmount: 0,
status: { [Op.in]: ['unpaid', 'partial'] },
billDate: new Date(),
settlementType: 'checkout',
sourceEventId: id,
remark: `退租押金退还 - ${renterName}`,
tenantId,
createBy,
updateBy: createBy
}, { transaction });
billsGenerated.push({ category: 'deposit_refund', amount: depositToRefund });
}
// 4. 更新房间状态为空房
await Room.update(
{ status: 'empty', rentalStatus: 'normal' },
{ where: { id: room.id }, transaction }
);
await transaction.commit();
response.success(res, '退租办理成功', {
rentalId: id,
roomId: room.id,
billsGenerated,
totalWaterBill: billsGenerated.filter(b => b.category === 'water').reduce((sum, b) => sum + b.amount, 0),
totalElectricityBill: billsGenerated.filter(b => b.category === 'electricity').reduce((sum, b) => sum + b.amount, 0),
depositRefund: depositToRefund
});
} catch (error) {
await transaction.rollback();
console.error('退租处理时出错:', error);
response.serverError(res, '退租处理失败', error);
}
};
// 续租接口
const renewRental = async (req, res) => {
const transaction = await require('../config/db').transaction();
try {
const { id } = req.params;
const {
newEndDate,
newRent,
newDeposit,
paymentType = 'monthly',
startDate,
operator,
remark,
waterStartReading,
electricStartReading
} = req.body;
const tenantId = req.user.tenantId;
const createBy = req.user.id;
// 获取原租约信息
const rental = await Rental.findOne({
where: { id, tenantId, isDeleted: 0 },
include: [
{ model: Room, where: { isDeleted: 0 } },
{ model: Renter }
],
transaction
});
if (!rental) {
await transaction.rollback();
return response.notFound(res, '租约不存在');
}
if (rental.status !== 'active') {
await transaction.rollback();
return response.badRequest(res, '只有生效中的租约可以续租');
}
// 检查是否有未支付的租金/水电费账单
const unpaidBills = await Bill.findAll({
where: {
rentalId: id,
status: 'unpaid',
category: { [Op.in]: ['rent', 'water', 'electricity'] },
isDeleted: 0
},
transaction
});
if (unpaidBills && unpaidBills.length > 0) {
await transaction.rollback();
return response.badRequest(res, `该租约有 ${unpaidBills.length} 笔未结清的账单,请先结清后再续租`);
}
const renterName = rental.Renter ? rental.Renter.name : '';
// 1. 更新原租约为已到期
await rental.update({
status: 'expired',
closeReason: 'renewed',
updateBy: createBy
}, { transaction });
// 2. 创建新租约
const newRental = await Rental.create({
roomId: rental.roomId,
renterId: rental.renterId,
startDate: startDate || rental.endDate,
endDate: newEndDate,
paymentType,
rent: newRent || rental.rent,
deposit: newDeposit !== undefined ? newDeposit : rental.deposit,
operator: operator || null,
waterMeterStart: waterStartReading || null,
electricityMeterStart: electricStartReading || null,
status: 'active',
prevRentalId: rental.id,
originRentalId: rental.originRentalId || rental.id,
remark: remark || `续租自原租约${rental.id}`,
tenantId,
createBy,
updateBy: createBy
}, { transaction });
// 3. 更新房间状态
await Room.update(
{ status: 'rented', rentalStatus: 'normal' },
{ where: { id: rental.roomId }, transaction }
);
// 4. 生成首期租金账单
const billStartDate = startDate || rental.endDate;
const billMonth = billStartDate.substring(0, 7);
await Bill.create({
billNo: generateBillNo(),
rentalId: newRental.id,
roomId: rental.roomId,
renterId: rental.renterId,
type: 'income',
category: 'rent',
receivableAmount: newRent || rental.rent,
receivedAmount: 0,
status: 'unpaid',
billDate: billStartDate,
billMonth: billMonth,
...buildBillingMeta('renew', rental.id),
remark: `续租首期租金 - ${renterName}`,
tenantId,
createBy,
updateBy: createBy
}, { transaction });
// 5. 如有押金变更,生成押金调整账单
const depositDiff = (newDeposit !== undefined ? newDeposit : rental.deposit) - rental.deposit;
if (depositDiff !== 0) {
await Bill.create({
billNo: generateBillNo(),
rentalId: newRental.id,
roomId: rental.roomId,
renterId: rental.renterId,
type: depositDiff > 0 ? 'income' : 'expense',
category: 'deposit',
receivableAmount: Math.abs(depositDiff),
receivedAmount: 0,
status: 'unpaid',
billDate: billStartDate,
billMonth: billMonth,
...buildBillingMeta('renew', rental.id),
remark: depositDiff > 0 ? `续租补交押金 - ${renterName}` : `续租退还押金 - ${renterName}`,
tenantId,
createBy,
updateBy: createBy
}, { transaction });
}
// 6. 记录新的水电表起始读数(如有)
if (waterStartReading !== null && waterStartReading !== undefined) {
await MeterReading.create({
roomId: rental.roomId,
renterId: rental.renterId,
rentalId: newRental.id,
meterType: 'water',
previousReading: 0,
currentReading: parseFloat(waterStartReading),
usage: 0,
unitPrice: 0,
amount: 0,
billMonth: billMonth,
readingDate: billStartDate,
remark: '续租时水表读数(起始读数)',
tenantId,
createBy,
updateBy: createBy
}, { transaction });
}
if (electricStartReading !== null && electricStartReading !== undefined) {
await MeterReading.create({
roomId: rental.roomId,
renterId: rental.renterId,
rentalId: newRental.id,
meterType: 'electricity',
previousReading: 0,
currentReading: parseFloat(electricStartReading),
usage: 0,
unitPrice: 0,
amount: 0,
billMonth: billMonth,
readingDate: billStartDate,
remark: '续租时电表读数(起始读数)',
tenantId,
createBy,
updateBy: createBy
}, { transaction });
}
await transaction.commit();
response.created(res, '续租办理成功', {
oldRentalId: rental.id,
newRentalId: newRental.id,
roomId: rental.roomId,
newEndDate: newEndDate,
newRent: newRent || rental.rent
});
} catch (error) {
await transaction.rollback();
console.error('续租处理时出错:', error);
response.serverError(res, '续租处理失败', error);
}
};
// 换房接口
const changeRoom = async (req, res) => {
const transaction = await require('../config/db').transaction();
try {
const { id } = req.params;
const {
newRoomId,
changeDate,
newRent,
newDeposit,
operator,
remark,
waterMeterEnd,
electricityMeterEnd
} = req.body;
const tenantId = req.user.tenantId;
const createBy = req.user.id;
// 获取原租约信息
const rental = await Rental.findOne({
where: { id, tenantId, isDeleted: 0 },
include: [
{ model: Room, where: { isDeleted: 0 } },
{ model: Renter }
],
transaction
});
if (!rental) {
await transaction.rollback();
return response.notFound(res, '租约不存在');
}
if (rental.status !== 'active') {
await transaction.rollback();
return response.badRequest(res, '只有生效中的租约可以换房');
}
const unsettledBills = await Bill.findAll({
where: {
rentalId: id,
tenantId,
status: { [Op.in]: ['unpaid', 'partial'] },
category: { [Op.in]: ['rent', 'water', 'electricity'] },
isDeleted: 0
},
transaction
});
if (unsettledBills && unsettledBills.length > 0) {
await transaction.rollback();
return response.badRequest(res, `该租约有 ${unsettledBills.length} 笔未结清的账单,请先结清后再换房`);
}
// 检查新房间
const newRoom = await Room.findOne({
where: { id: newRoomId, tenantId, isDeleted: 0 },
transaction
});
if (!newRoom) {
await transaction.rollback();
return response.notFound(res, '新房间不存在');
}
if (newRoom.status === 'rented') {
await transaction.rollback();
return response.badRequest(res, '新房间已在租,请选择其他房间');
}
const renterName = rental.Renter ? rental.Renter.name : '';
const oldRoomId = rental.roomId;
// 生成账单编号
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(Math.random() * 10000).toString().padStart(4, '0');
return `B${year}${month}${day}${random}`;
};
// 1. 结算原房间的水电费
if ((waterMeterEnd || electricityMeterEnd) && changeDate) {
// 获取原房间最新的抄表记录
if (waterMeterEnd) {
const waterReading = await MeterReading.findOne({
where: { roomId: oldRoomId, meterType: 'water', rentalId: id, isDeleted: 0 },
order: [['createTime', 'DESC']],
transaction
});
if (waterReading && waterMeterEnd > waterReading.currentReading) {
const waterUsage = waterMeterEnd - waterReading.currentReading;
const waterAmount = waterUsage * parseFloat(waterReading.unitPrice || 0);
await Bill.create({
billNo: generateBillNo(),
rentalId: id,
roomId: oldRoomId,
renterId: rental.renterId,
type: 'income',
category: 'water',
receivableAmount: waterAmount,
receivedAmount: 0,
status: 'unpaid',
billDate: changeDate,
billMonth: changeDate.substring(0, 7),
...buildBillingMeta('change_room', rental.id),
remark: `换房结算水费(${renterName},用量:${waterUsage.toFixed(2)}吨)`,
tenantId,
createBy,
updateBy: createBy
}, { transaction });
}
}
if (electricityMeterEnd) {
const electricityReading = await MeterReading.findOne({
where: { roomId: oldRoomId, meterType: 'electricity', rentalId: id, isDeleted: 0 },
order: [['createTime', 'DESC']],
transaction
});
if (electricityReading && electricityMeterEnd > electricityReading.currentReading) {
const electricityUsage = electricityMeterEnd - electricityReading.currentReading;
const electricityAmount = electricityUsage * parseFloat(electricityReading.unitPrice || 0);
await Bill.create({
billNo: generateBillNo(),
rentalId: id,
roomId: oldRoomId,
renterId: rental.renterId,
type: 'income',
category: 'electricity',
receivableAmount: electricityAmount,
receivedAmount: 0,
status: 'unpaid',
billDate: changeDate,
billMonth: changeDate.substring(0, 7),
...buildBillingMeta('change_room', rental.id),
remark: `换房结算电费(${renterName},用量:${electricityUsage.toFixed(2)}度)`,
tenantId,
createBy,
updateBy: createBy
}, { transaction });
}
}
}
// 2. 更新原房间状态为空房
await Room.update(
{ status: 'empty', rentalStatus: 'normal' },
{ where: { id: oldRoomId }, transaction }
);
// 3. 关闭原租约(关旧)
await rental.update({
status: 'terminated',
closeReason: 'changed_room',
remark: `${rental.remark || ''}${remark ? `\n换房备注:${remark}` : ''}`.trim(),
updateBy: createBy
}, { transaction });
// 4. 新建租约(开新)
const newRental = await Rental.create({
roomId: newRoomId,
renterId: rental.renterId,
startDate: changeDate || new Date(),
endDate: rental.endDate,
paymentType: rental.paymentType,
rent: newRent || rental.rent,
deposit: newDeposit !== undefined ? newDeposit : rental.deposit,
operator: operator || rental.operator,
waterMeterStart: waterMeterEnd || null,
electricityMeterStart: electricityMeterEnd || null,
status: 'active',
prevRentalId: rental.id,
originRentalId: rental.originRentalId || rental.id,
remark: `由租约${rental.id}换房新建`,
tenantId,
createBy,
updateBy: createBy
}, { transaction });
// 5. 生成新房间租金账单(如有租金)
const newRoomRent = newRent || rental.rent;
if (newRoomRent > 0) {
const billStartDate = changeDate || new Date();
const billMonth = rentalBillingService.billMonthOf(billStartDate);
await Bill.create({
billNo: generateBillNo(),
rentalId: newRental.id,
roomId: newRoomId,
renterId: rental.renterId,
type: 'income',
category: 'rent',
receivableAmount: newRoomRent,
receivedAmount: 0,
status: 'unpaid',
billDate: billStartDate,
billMonth: billMonth,
...buildBillingMeta('change_room', rental.id),
remark: `换房首期租金 - ${renterName}`,
tenantId,
createBy,
updateBy: createBy
}, { transaction });
}
// 5. 更新新房间状态为已租
await Room.update(
{ status: 'rented', rentalStatus: 'normal' },
{ where: { id: newRoomId }, transaction }
);
await transaction.commit();
response.success(res, '换房办理成功', {
oldRentalId: rental.id,
newRentalId: newRental.id,
oldRoomId: oldRoomId,
newRoomId: newRoomId,
changeDate: changeDate
});
} catch (error) {
await transaction.rollback();
console.error('换房处理时出错:', error);
response.serverError(res, '换房处理失败', error);
}
};
// 获取所有租房(不分页)
const listRentals = async (req, res) => {
try {
// 先检查并更新租房状态
await checkAndUpdateRentalStatus(req.user.tenantId);
const {
apartmentId,
roomId,
renterName,
status,
startDateFrom,
startDateTo,
endDateFrom,
endDateTo
} = req.query;
// 构建查询条件
const where = { tenantId: req.user.tenantId, isDeleted: 0 };
if (status) {
where.status = status;
}
if (roomId) {
where.roomId = roomId;
}
if (startDateFrom && startDateTo) {
where.startDate = { [Op.between]: [new Date(startDateFrom), new Date(startDateTo)] };
}
if (endDateFrom && endDateTo) {
where.endDate = { [Op.between]: [new Date(endDateFrom), new Date(endDateTo)] };
}
// 构建包含关系
const include = [
{
model: Room,
where: {
isDeleted: 0,
...(apartmentId ? { apartmentId } : {})
},
include: [
{
model: Apartment,
where: { isDeleted: 0 }
}
]
},
{
model: Renter,
where: renterName ? { name: { [Op.like]: `%${renterName}%` } } : undefined,
required: !!renterName
}
];
// 查询租房数据
const rentals = await Rental.findAll({
where,
include,
order: [['createTime', 'DESC']] // 按创建时间倒序排序
});
// 格式化数据
const formattedRentals = rentals.map(formatRentalData);
// 返回结果
response.success(res, '获取成功', formattedRentals);
} catch (error) {
response.serverError(res, '获取租房列表失败', error);
}
};
// 创建租房(包含租客、水电表读数)- 整合接口
// 注意:入住时只记录水电表起始读数,不生成账单,水电费通过后续抄表生成
const createRentalWithRenter = async (req, res) => {
const transaction = await require('../config/db').transaction();
try {
console.log('接收到的整合请求数据:', req.body);
const {
// 租客信息
renterName,
renterPhone,
renterIdCard,
// 租房信息
roomId,
paymentType = 'monthly',
startDate,
endDate,
rent,
deposit = 0,
operator,
remark,
// 水电表读数(入住时记录,不生成账单)
waterStartReading,
electricStartReading
} = req.body;
const tenantId = req.user.tenantId;
const createBy = req.user.id;
// 参数验证
if (!renterName) {
await transaction.rollback();
return response.badRequest(res, '租客姓名不能为空');
}
if (!roomId) {
await transaction.rollback();
return response.badRequest(res, '房间ID不能为空');
}
if (!startDate || !endDate) {
await transaction.rollback();
return response.badRequest(res, '开始日期和结束日期不能为空');
}
if (!rent) {
await transaction.rollback();
return response.badRequest(res, '租金不能为空');
}
// 检查房间状态
const room = await Room.findOne({
where: { id: roomId, tenantId, isDeleted: 0 },
transaction
});
if (!room) {
await transaction.rollback();
return response.notFound(res, '房间不存在');
}
if (room.status === 'rented') {
await transaction.rollback();
return response.badRequest(res, '该房间已在租,请选择其他房间');
}
const parsedRoomId = parseInt(roomId);
if (isNaN(parsedRoomId)) {
await transaction.rollback();
return response.badRequest(res, '无效的房间ID');
}
// 1. 创建租客
const renterData = {
name: renterName,
phone: renterPhone || null,
idCard: renterIdCard || null,
tenantId,
createBy,
status: 'active'
};
const renter = await Renter.create(renterData, { transaction });
console.log('租客创建成功:', renter.id);
// 2. 创建租房记录
const rentalData = {
roomId: parsedRoomId,
renterId: renter.id,
startDate,
endDate,
paymentType,
rent,
deposit,
operator: operator || null,
waterMeterStart: waterStartReading || null,
electricityMeterStart: electricStartReading || null,
status: 'active',
remark: remark || null,
tenantId,
createBy,
updateBy: createBy
};
const rental = await Rental.create(rentalData, { transaction });
console.log('租房记录创建成功:', rental.id);
// 3. 更新房间状态为已租
await Room.update(
{ status: 'rented', rentalStatus: 'normal' },
{ where: { id: parsedRoomId }, transaction }
);
// 生成账单编号
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(Math.random() * 10000).toString().padStart(4, '0')
return `B${year}${month}${day}${random}`
}
// 4. 创建首期租金账单(根据付款周期生成相应期数)
// 简化处理:默认只生成首期账单,后续通过定时任务或手动生成
const firstBillDate = startDate;
const firstBillMonth = startDate.substring(0, 7);
await Bill.create({
billNo: generateBillNo(),
rentalId: rental.id,
roomId: parsedRoomId,
renterId: renter.id,
type: 'income',
category: 'rent',
receivableAmount: rent,
receivedAmount: 0,
status: 'unpaid',
billDate: firstBillDate,
billMonth: firstBillMonth,
settlementType: 'normal',
remark: `租约首期租金 - ${renterName}`,
tenantId,
createBy,
updateBy: createBy
}, { transaction });
// 5. 创建押金账单(如果有押金)
if (deposit > 0) {
await Bill.create({
billNo: generateBillNo(),
rentalId: rental.id,
roomId: parsedRoomId,
renterId: renter.id,
type: 'income',
category: 'deposit',
receivableAmount: deposit,
receivedAmount: 0,
status: 'unpaid',
billDate: firstBillDate,
billMonth: firstBillMonth,
settlementType: 'normal',
remark: `租约押金 - ${renterName}`,
tenantId,
createBy,
updateBy: createBy
}, { transaction });
}
// 6. 记录水电表起始读数(仅记录,不生成账单)
// 水电费通过后续抄表管理生成账单
// 创建水表起始读数记录
if (waterStartReading !== null && waterStartReading !== '' && waterStartReading !== undefined) {
await MeterReading.create({
roomId: parsedRoomId,
renterId: renter.id,
rentalId: rental.id,
meterType: 'water',
previousReading: 0,
currentReading: parseFloat(waterStartReading),
usage: 0,
unitPrice: 0,
amount: 0,
billMonth: startDate.substring(0, 7),
readingDate: startDate,
remark: '入住时水表读数(起始读数)',
tenantId,
createBy,
updateBy: createBy
}, { transaction });
console.log('水表起始读数记录创建成功');
}
// 创建电表起始读数记录
if (electricStartReading !== null && electricStartReading !== '' && electricStartReading !== undefined) {
await MeterReading.create({
roomId: parsedRoomId,
renterId: renter.id,
rentalId: rental.id,
meterType: 'electricity',
previousReading: 0,
currentReading: parseFloat(electricStartReading),
usage: 0,
unitPrice: 0,
amount: 0,
billMonth: startDate.substring(0, 7),
readingDate: startDate,
remark: '入住时电表读数(起始读数)',
tenantId,
createBy,
updateBy: createBy
}, { transaction });
console.log('电表起始读数记录创建成功');
}
// 提交事务
await transaction.commit();
response.created(res, '入住办理成功(首期租金和押金账单已生成,水电表读数已记录)', {
rentalId: rental.id,
renterId: renter.id,
roomId: parsedRoomId,
billsGenerated: {
rent: true,
deposit: deposit > 0,
meterReadings: !!(waterStartReading || electricStartReading)
}
});
} catch (error) {
// 回滚事务
await transaction.rollback();
console.error('创建租房(整合)时出错:', error);
response.serverError(res, '创建租房失败', error);
}
};
module.exports = {
getAllRentals,
listRentals,
getRentalById,
createRental,
updateRental,
deleteRental,
terminateRental,
createRentalWithRenter,
renewRental,
changeRoom
};