rentease-backend-new/controllers/rentalController.js

1464 lines
43 KiB
JavaScript
Raw Permalink Normal View History

2026-04-20 06:43:09 +00:00
const { Rental, Room, Apartment, Bill, Transaction, Renter, MeterReading } = require('../models');
const { Op } = require('sequelize');
2026-04-22 06:48:32 +00:00
const response = require('../utils/response');
2026-06-11 13:45:33 +00:00
const rentalBillingService = require('../services/rentalBillingService');
2026-04-20 06:43:09 +00:00
// 格式化时间(考虑时区,转换为北京时间)
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];
};
2026-05-09 09:01:41 +00:00
// 格式化日期时间(年月日时分秒)
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);
};
2026-04-20 06:43:09 +00:00
// 格式化租房数据
const formatRentalData = (rental) => {
const formattedRental = {
...rental.toJSON(),
startDate: formatDate(rental.startDate),
endDate: formatDate(rental.endDate),
2026-05-09 09:01:41 +00:00
createTime: formatDateTime(rental.createTime),
updateTime: formatDateTime(rental.updateTime)
2026-04-20 06:43:09 +00:00
};
return formattedRental;
};
2026-06-11 13:45:33 +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(Math.random() * 10000).toString().padStart(4, '0');
return `B${year}${month}${day}${random}`;
};
const buildBillingMeta = (settlementType, sourceEventId = null) => ({
settlementType,
sourceEventId
});
2026-04-20 06:43:09 +00:00
// 检查并更新租房状态
2026-05-09 09:01:41 +00:00
const checkAndUpdateRentalStatus = async (tenantId) => {
2026-04-20 06:43:09 +00:00
try {
// 获取当前日期
const currentDate = new Date();
// 计算5天后的日期
const fiveDaysLater = new Date();
fiveDaysLater.setDate(currentDate.getDate() + 5);
2026-05-09 09:01:41 +00:00
// 查找该租户下所有活跃的租房记录
2026-04-20 06:43:09 +00:00
const rentals = await Rental.findAll({
2026-05-09 09:01:41 +00:00
where: { tenantId, status: 'active', isDeleted: 0 },
2026-04-20 06:43:09 +00:00
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 {
// 先检查并更新租房状态
2026-05-09 09:01:41 +00:00
await checkAndUpdateRentalStatus(req.user.tenantId);
2026-04-20 06:43:09 +00:00
const {
apartmentId,
roomId,
renterName,
status,
startDateFrom,
startDateTo,
endDateFrom,
endDateTo,
page = 1,
pageSize = 10
} = req.query;
// 构建查询条件
2026-05-09 09:01:41 +00:00
const where = { tenantId: req.user.tenantId, isDeleted: 0 };
2026-04-20 06:43:09 +00:00
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);
// 返回结果
2026-04-22 06:48:32 +00:00
response.success(res, '获取成功', {
list: formattedRentals,
2026-04-20 06:43:09 +00:00
total: count,
page: parseInt(page),
pageSize: parseInt(pageSize)
});
} catch (error) {
2026-04-22 06:48:32 +00:00
response.serverError(res, '获取租房列表失败', error);
2026-04-20 06:43:09 +00:00
}
};
// 获取单个租房
const getRentalById = async (req, res) => {
try {
const { id } = req.params;
const rental = await Rental.findOne({
2026-05-09 09:01:41 +00:00
where: { id, tenantId: req.user.tenantId, isDeleted: 0 },
2026-04-20 06:43:09 +00:00
include: [
{
model: Room,
where: { isDeleted: 0 },
include: [
{
2026-06-11 13:45:33 +00:00
model: Apartment
2026-04-20 06:43:09 +00:00
}
]
},
{
model: Renter
}
]
});
if (!rental) {
2026-04-22 06:48:32 +00:00
return response.notFound(res, '租房记录不存在');
2026-04-20 06:43:09 +00:00
}
const formattedRental = formatRentalData(rental);
2026-04-22 06:48:32 +00:00
response.success(res, '获取成功', formattedRental);
2026-04-20 06:43:09 +00:00
} catch (error) {
2026-04-22 06:48:32 +00:00
response.serverError(res, '获取租房详情失败', error);
2026-04-20 06:43:09 +00:00
}
};
// 创建租房
const createRental = async (req, res) => {
try {
console.log('接收到的请求数据:', req.body);
// 直接使用req.body中的数据
const body = req.body;
// 检查请求体是否存在
if (!body) {
2026-04-22 06:48:32 +00:00
return response.badRequest(res, '请求体不能为空');
2026-04-20 06:43:09 +00:00
}
// 检查所有必要参数
if (!body.roomId) {
2026-04-22 06:48:32 +00:00
return response.badRequest(res, '缺少房间ID');
2026-04-20 06:43:09 +00:00
}
if (!body.renterId) {
2026-04-22 06:48:32 +00:00
return response.badRequest(res, '缺少租客ID');
2026-04-20 06:43:09 +00:00
}
if (!body.startDate) {
2026-04-22 06:48:32 +00:00
return response.badRequest(res, '缺少开始日期');
2026-04-20 06:43:09 +00:00
}
if (!body.endDate) {
2026-04-22 06:48:32 +00:00
return response.badRequest(res, '缺少结束日期');
2026-04-20 06:43:09 +00:00
}
if (!body.rent) {
2026-04-22 06:48:32 +00:00
return response.badRequest(res, '缺少租金');
2026-04-20 06:43:09 +00:00
}
// 转换roomId和renterId为整数类型
const parsedRoomId = parseInt(body.roomId);
const parsedRenterId = parseInt(body.renterId);
if (isNaN(parsedRoomId)) {
2026-04-22 06:48:32 +00:00
return response.badRequest(res, '无效的房间ID');
2026-04-20 06:43:09 +00:00
}
if (isNaN(parsedRenterId)) {
2026-04-22 06:48:32 +00:00
return response.badRequest(res, '无效的租客ID');
2026-04-20 06:43:09 +00:00
}
2026-06-11 13:45:33 +00:00
// 检查房间是否存在
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, '该房间已有进行中的租赁合同');
}
2026-04-20 06:43:09 +00:00
// 处理押金为空时设置为0
const deposit = body.deposit || 0;
// 获取租客信息用于账单备注
const renter = await Renter.findByPk(parsedRenterId);
const renterName = renter ? renter.name : '';
2026-06-11 13:45:33 +00:00
var transaction;
2026-04-20 06:43:09 +00:00
// 创建租房记录
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
});
2026-06-11 13:45:33 +00:00
transaction = await require('../config/db').transaction();
2026-04-20 06:43:09 +00:00
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,
2026-05-09 09:01:41 +00:00
tenantId: req.user.tenantId,
2026-04-20 06:43:09 +00:00
createBy: req.user.id,
updateBy: req.user.id
2026-06-11 13:45:33 +00:00
}, { transaction });
2026-04-20 06:43:09 +00:00
console.log('租房记录:', rental);
// 更新房间状态为已租
2026-06-11 13:45:33 +00:00
await Room.update({ status: 'rented', rentalStatus: 'normal' }, { where: { id: parsedRoomId }, transaction });
2026-04-20 06:43:09 +00:00
// 自动生成租金账单
await Bill.create({
2026-05-09 09:01:41 +00:00
billNo: 'B' + Date.now(),
2026-04-20 06:43:09 +00:00
rentalId: rental.id,
roomId: parsedRoomId,
2026-05-09 09:01:41 +00:00
renterId: parsedRenterId,
2026-04-20 06:43:09 +00:00
type: 'income',
category: 'rent',
2026-05-09 09:01:41 +00:00
receivableAmount: body.rent,
receivedAmount: 0,
status: 'unpaid',
billDate: body.startDate,
billMonth: body.startDate.substring(0, 7),
2026-06-11 13:45:33 +00:00
settlementType: 'normal',
2026-04-20 06:43:09 +00:00
remark: `租约租金 - ${renterName}`,
tenantId: req.user.tenantId,
createBy: req.user.id,
updateBy: req.user.id
2026-06-11 13:45:33 +00:00
}, { transaction });
2026-04-20 06:43:09 +00:00
// 自动生成押金账单(如果有押金)
if (deposit > 0) {
await Bill.create({
2026-05-09 09:01:41 +00:00
billNo: 'B' + Date.now() + '1',
2026-04-20 06:43:09 +00:00
rentalId: rental.id,
roomId: parsedRoomId,
2026-05-09 09:01:41 +00:00
renterId: parsedRenterId,
2026-04-20 06:43:09 +00:00
type: 'income',
category: 'deposit',
2026-05-09 09:01:41 +00:00
receivableAmount: deposit,
receivedAmount: 0,
status: 'unpaid',
billDate: body.startDate,
billMonth: body.startDate.substring(0, 7),
2026-06-11 13:45:33 +00:00
settlementType: 'normal',
2026-04-20 06:43:09 +00:00
remark: `租约押金 - ${renterName}`,
tenantId: req.user.tenantId,
createBy: req.user.id,
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();
2026-04-22 06:48:32 +00:00
response.created(res, '创建成功', rental);
2026-04-20 06:43:09 +00:00
} catch (error) {
2026-06-11 13:45:33 +00:00
if (typeof transaction !== 'undefined' && transaction) {
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 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;
2026-06-11 13:45:33 +00:00
2026-04-20 06:43:09 +00:00
const rental = await Rental.findOne({
2026-06-11 13:45:33 +00:00
where: { id, tenantId: req.user.tenantId, isDeleted: 0 },
include: [{ model: Room }]
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 (!rental) {
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 (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 }
);
}
2026-04-20 06:43:09 +00:00
// 处理押金为空时设置为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
2026-06-11 13:45:33 +00:00
}, { transaction });
const hasPaidBills = await Bill.count({
where: {
rentalId: id,
tenantId: req.user.tenantId,
status: { [Op.in]: ['paid', 'partial'] },
isDeleted: 0
},
transaction
2026-04-20 06:43:09 +00:00
});
2026-06-11 13:45:33 +00:00
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();
2026-04-22 06:48:32 +00:00
response.success(res, '更新成功', rental);
2026-04-20 06:43:09 +00:00
} catch (error) {
2026-06-11 13:45:33 +00:00
if (typeof transaction !== 'undefined' && transaction) {
await transaction.rollback();
}
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 deleteRental = 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 rental = await Rental.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 (!rental) {
2026-06-11 13:45:33 +00:00
await transaction.rollback();
return response.notFound(res, '租约不存在');
2026-04-20 06:43:09 +00:00
}
2026-06-11 13:45:33 +00:00
await rentalBillingService.assertRentalCanBeDeleted({
rentalId: id,
tenantId: req.user.tenantId,
transaction
});
await rentalBillingService.cancelUnpaidBillsForRental({
rentalId: id,
tenantId: req.user.tenantId,
operatorId: req.user.id,
transaction
});
2026-04-20 06:43:09 +00:00
await rental.update({
isDeleted: 1,
updateBy: req.user.id
2026-06-11 13:45:33 +00:00
}, { transaction });
const otherActiveRental = await Rental.findOne({
where: {
id: { [Op.ne]: id },
roomId: rental.roomId,
tenantId: req.user.tenantId,
status: 'active',
isDeleted: 0
},
transaction
2026-04-20 06:43:09 +00:00
});
2026-06-11 13:45:33 +00:00
if (!otherActiveRental) {
await Room.update(
{ status: 'empty', rentalStatus: 'normal' },
{ where: { id: rental.roomId, tenantId: req.user.tenantId }, transaction }
);
}
await transaction.commit();
response.success(res, '租约删除成功');
2026-04-20 06:43:09 +00:00
} catch (error) {
2026-06-11 13:45:33 +00:00
await transaction.rollback();
if (error.statusCode === 400) {
return response.badRequest(res, error.message);
}
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 terminateRental = 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;
2026-06-11 13:45:33 +00:00
const {
waterMeterEnd,
electricityMeterEnd,
refundDeposit,
remark
} = req.body;
2026-04-20 06:43:09 +00:00
const rental = await Rental.findOne({
2026-05-09 09:01:41 +00:00
where: { id, tenantId: req.user.tenantId, isDeleted: 0 },
2026-04-20 06:43:09 +00:00
include: [
{
model: Room,
where: { isDeleted: 0 },
2026-06-11 13:45:33 +00:00
include: [{ model: Apartment, where: { isDeleted: 0 } }]
2026-04-20 06:43:09 +00:00
},
2026-06-11 13:45:33 +00:00
{ model: Renter }
],
transaction
2026-04-20 06:43:09 +00:00
});
if (!rental) {
2026-06-11 13:45:33 +00:00
await transaction.rollback();
return response.notFound(res, '租约不存在');
2026-04-20 06:43:09 +00:00
}
if (rental.status !== 'active') {
2026-06-11 13:45:33 +00:00
await transaction.rollback();
2026-04-22 06:48:32 +00:00
return response.badRequest(res, '只有生效中的租约可以退租');
2026-04-20 06:43:09 +00:00
}
2026-06-11 13:45:33 +00:00
// 检查是否有未支付的租金/水电费账单
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} 笔未结清的账单,请先结清后再退租`);
}
2026-04-20 06:43:09 +00:00
const room = rental.Room;
const apartment = room.Apartment;
const renterName = rental.Renter ? rental.Renter.name : '';
2026-06-11 13:45:33 +00:00
const createBy = req.user.id;
const tenantId = req.user.tenantId;
2026-04-20 06:43:09 +00:00
2026-06-11 13:45:33 +00:00
const billsGenerated = [];
// 1. 更新租约信息
2026-04-20 06:43:09 +00:00
await rental.update({
2026-06-11 13:45:33 +00:00
status: 'terminated',
closeReason: 'terminated',
2026-04-20 06:43:09 +00:00
waterMeterEnd: waterMeterEnd || null,
electricityMeterEnd: electricityMeterEnd || null,
remark: remark ? `${rental.remark || ''}\n退租备注:${remark}` : rental.remark,
2026-06-11 13:45:33 +00:00
updateBy: createBy
}, { transaction });
2026-04-20 06:43:09 +00:00
2026-06-11 13:45:33 +00:00
// 2. 生成水电费账单(如有读数差异)
2026-04-20 06:43:09 +00:00
if (waterMeterEnd && rental.waterMeterStart) {
const waterUsage = parseFloat(waterMeterEnd) - parseFloat(rental.waterMeterStart);
2026-06-11 13:45:33 +00:00
if (waterUsage > 0) {
const waterAmount = waterUsage * parseFloat(apartment.waterPrice || 0);
const bill = await Bill.create({
billNo: generateBillNo(),
rentalId: id,
2026-04-20 06:43:09 +00:00
roomId: room.id,
2026-05-09 09:01:41 +00:00
renterId: rental.renterId,
2026-04-20 06:43:09 +00:00
type: 'income',
category: 'water',
2026-05-09 09:01:41 +00:00
receivableAmount: waterAmount,
receivedAmount: 0,
status: 'unpaid',
billDate: new Date(),
2026-06-11 13:45:33 +00:00
settlementType: 'checkout',
sourceEventId: id,
2026-04-20 06:43:09 +00:00
remark: `退租水费 - ${renterName}(用量:${waterUsage.toFixed(2)}吨)`,
2026-06-11 13:45:33 +00:00
tenantId,
createBy,
updateBy: createBy
}, { transaction });
billsGenerated.push({ category: 'water', amount: waterAmount });
2026-04-20 06:43:09 +00:00
}
}
if (electricityMeterEnd && rental.electricityMeterStart) {
const electricityUsage = parseFloat(electricityMeterEnd) - parseFloat(rental.electricityMeterStart);
2026-06-11 13:45:33 +00:00
if (electricityUsage > 0) {
const electricityAmount = electricityUsage * parseFloat(apartment.electricityPrice || 0);
const bill = await Bill.create({
billNo: generateBillNo(),
rentalId: id,
2026-04-20 06:43:09 +00:00
roomId: room.id,
2026-05-09 09:01:41 +00:00
renterId: rental.renterId,
2026-04-20 06:43:09 +00:00
type: 'income',
category: 'electricity',
2026-05-09 09:01:41 +00:00
receivableAmount: electricityAmount,
receivedAmount: 0,
status: 'unpaid',
billDate: new Date(),
2026-06-11 13:45:33 +00:00
settlementType: 'checkout',
sourceEventId: id,
2026-04-20 06:43:09 +00:00
remark: `退租电费 - ${renterName}(用量:${electricityUsage.toFixed(2)}度)`,
2026-06-11 13:45:33 +00:00
tenantId,
createBy,
updateBy: createBy
}, { transaction });
billsGenerated.push({ category: 'electricity', amount: electricityAmount });
2026-04-20 06:43:09 +00:00
}
}
2026-06-11 13:45:33 +00:00
// 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. 更新房间状态为空房
2026-04-20 06:43:09 +00:00
await Room.update(
{ status: 'empty', rentalStatus: 'normal' },
2026-06-11 13:45:33 +00:00
{ where: { id: room.id }, transaction }
2026-04-20 06:43:09 +00:00
);
2026-06-11 13:45:33 +00:00
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
});
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
}
};
2026-06-11 13:45:33 +00:00
// 续租接口
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);
}
};
2026-04-20 06:43:09 +00:00
// 获取所有租房(不分页)
const listRentals = async (req, res) => {
try {
// 先检查并更新租房状态
2026-05-09 09:01:41 +00:00
await checkAndUpdateRentalStatus(req.user.tenantId);
2026-04-20 06:43:09 +00:00
const {
apartmentId,
roomId,
renterName,
status,
startDateFrom,
startDateTo,
endDateFrom,
endDateTo
} = req.query;
// 构建查询条件
2026-05-09 09:01:41 +00:00
const where = { tenantId: req.user.tenantId, isDeleted: 0 };
2026-04-20 06:43:09 +00:00
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);
// 返回结果
2026-04-22 06:48:32 +00:00
response.success(res, '获取成功', formattedRentals);
2026-04-20 06:43:09 +00:00
} catch (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 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,
2026-06-11 13:45:33 +00:00
// 水电表读数(入住时记录,不生成账单)
2026-04-20 06:43:09 +00:00
waterStartReading,
electricStartReading
} = req.body;
const tenantId = req.user.tenantId;
const createBy = req.user.id;
// 参数验证
if (!renterName) {
await transaction.rollback();
2026-04-22 06:48:32 +00:00
return response.badRequest(res, '租客姓名不能为空');
2026-04-20 06:43:09 +00:00
}
if (!roomId) {
await transaction.rollback();
2026-04-22 06:48:32 +00:00
return response.badRequest(res, '房间ID不能为空');
2026-04-20 06:43:09 +00:00
}
if (!startDate || !endDate) {
await transaction.rollback();
2026-04-22 06:48:32 +00:00
return response.badRequest(res, '开始日期和结束日期不能为空');
2026-04-20 06:43:09 +00:00
}
if (!rent) {
await transaction.rollback();
2026-04-22 06:48:32 +00:00
return response.badRequest(res, '租金不能为空');
2026-04-20 06:43:09 +00:00
}
2026-06-11 13:45:33 +00:00
// 检查房间状态
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, '该房间已在租,请选择其他房间');
}
2026-04-20 06:43:09 +00:00
const parsedRoomId = parseInt(roomId);
if (isNaN(parsedRoomId)) {
await transaction.rollback();
2026-04-22 06:48:32 +00:00
return response.badRequest(res, '无效的房间ID');
2026-04-20 06:43:09 +00:00
}
// 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}`
}
2026-06-11 13:45:33 +00:00
// 4. 创建首期租金账单(根据付款周期生成相应期数)
// 简化处理:默认只生成首期账单,后续通过定时任务或手动生成
const firstBillDate = startDate;
const firstBillMonth = startDate.substring(0, 7);
2026-04-20 06:43:09 +00:00
await Bill.create({
billNo: generateBillNo(),
rentalId: rental.id,
roomId: parsedRoomId,
renterId: renter.id,
type: 'income',
category: 'rent',
receivableAmount: rent,
receivedAmount: 0,
status: 'unpaid',
2026-06-11 13:45:33 +00:00
billDate: firstBillDate,
billMonth: firstBillMonth,
settlementType: 'normal',
remark: `租约首期租金 - ${renterName}`,
2026-04-20 06:43:09 +00:00
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',
2026-06-11 13:45:33 +00:00
billDate: firstBillDate,
billMonth: firstBillMonth,
settlementType: 'normal',
2026-04-20 06:43:09 +00:00
remark: `租约押金 - ${renterName}`,
tenantId,
createBy,
updateBy: createBy
}, { transaction });
}
2026-06-11 13:45:33 +00:00
// 6. 记录水电表起始读数(仅记录,不生成账单)
// 水电费通过后续抄表管理生成账单
// 创建水表起始读数记录
2026-04-20 06:43:09 +00:00
if (waterStartReading !== null && waterStartReading !== '' && waterStartReading !== undefined) {
await MeterReading.create({
roomId: parsedRoomId,
renterId: renter.id,
rentalId: rental.id,
meterType: 'water',
previousReading: 0,
2026-06-11 13:45:33 +00:00
currentReading: parseFloat(waterStartReading),
2026-04-20 06:43:09 +00:00
usage: 0,
2026-06-11 13:45:33 +00:00
unitPrice: 0,
2026-04-20 06:43:09 +00:00
amount: 0,
billMonth: startDate.substring(0, 7),
readingDate: startDate,
2026-06-11 13:45:33 +00:00
remark: '入住时水表读数(起始读数)',
2026-04-20 06:43:09 +00:00
tenantId,
createBy,
updateBy: createBy
}, { transaction });
2026-06-11 13:45:33 +00:00
console.log('水表起始读数记录创建成功');
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 (electricStartReading !== null && electricStartReading !== '' && electricStartReading !== undefined) {
await MeterReading.create({
roomId: parsedRoomId,
renterId: renter.id,
rentalId: rental.id,
meterType: 'electricity',
previousReading: 0,
2026-06-11 13:45:33 +00:00
currentReading: parseFloat(electricStartReading),
2026-04-20 06:43:09 +00:00
usage: 0,
2026-06-11 13:45:33 +00:00
unitPrice: 0,
2026-04-20 06:43:09 +00:00
amount: 0,
billMonth: startDate.substring(0, 7),
readingDate: startDate,
2026-06-11 13:45:33 +00:00
remark: '入住时电表读数(起始读数)',
2026-04-20 06:43:09 +00:00
tenantId,
createBy,
updateBy: createBy
}, { transaction });
2026-06-11 13:45:33 +00:00
console.log('电表起始读数记录创建成功');
2026-04-20 06:43:09 +00:00
}
// 提交事务
await transaction.commit();
2026-06-11 13:45:33 +00:00
response.created(res, '入住办理成功(首期租金和押金账单已生成,水电表读数已记录)', {
2026-04-22 06:48:32 +00:00
rentalId: rental.id,
renterId: renter.id,
2026-06-11 13:45:33 +00:00
roomId: parsedRoomId,
billsGenerated: {
rent: true,
deposit: deposit > 0,
meterReadings: !!(waterStartReading || electricStartReading)
}
2026-04-20 06:43:09 +00:00
});
} catch (error) {
// 回滚事务
await transaction.rollback();
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 = {
getAllRentals,
listRentals,
getRentalById,
createRental,
updateRental,
deleteRental,
terminateRental,
2026-06-11 13:45:33 +00:00
createRentalWithRenter,
renewRental,
changeRoom
2026-04-20 06:43:09 +00:00
};